package org.sanguine.study.hometasks.first;

import static java.lang.Math.abs;
import static java.lang.Math.round;

import java.io.PrintWriter;
import java.util.Arrays;
import java.util.Random;

public class MatrixOfDouble {
	protected double[][] matrix;
	private int maxRangeBegin;
	private int maxLength;

	public MatrixOfDouble(int numberOfRows, int numberOfColumns) {
		this.matrix = new double[numberOfRows][numberOfColumns];
	}
	
	public MatrixOfDouble createRandomMatrix(int n) {
		if (matrix != null) {
			Random rnd = new Random();
			
			for (int i = 0; i < matrix.length; i++)
				for (int j = 0; j < matrix[i].length; j++) {
					matrix[i][j] = rnd.nextDouble() * n * (rnd.nextInt(2) - 1);
				}
		}
		return this;
	}
	
	public MatrixOfDouble setMatrix(double[][] m) {
		matrix = m;
		return this;
	}
	
	public MatrixOfDouble sortRowsByCharacteristicDescending() {
		if (matrix != null) {
			class Entry implements Comparable<Entry>{
				double key;
				int column;
				@Override
				public int compareTo(Entry o) {
					return (key == o.key) ? 0 : (key < o.key) ? 1 : -1;
				}
				
				public Entry(double key, int reference) {
					this.key = key;
					this.column = reference;
				}
			}

			Entry[] references = new Entry[matrix[0].length];
			
			for (int i = 0; i < matrix[0].length; i++) references[i] = new Entry(computeCharacteristic(i), i);
			
			Arrays.sort(references);
			
			//move columns according to references
			
			for (int i = 0; i  < matrix.length; i++) {
				double[] newRow = new double[matrix[i].length];
				if (newRow.length != references.length) throw new IllegalStateException("Matrix must be orthogonal");

				for (int j = 0; j < references.length; j++) {
					newRow[j] = matrix[i][references[j].column];
				}
				matrix[i] = newRow;
			}
		}
		return this;
	}
	
	public MatrixOfDouble sortRowsBySumOfElements() {
		if (matrix != null) {
	
			class Entry implements Comparable<Entry>{
				double key;
				int row;
				@Override
				public int compareTo(Entry o) {
					return (key == o.key) ? 0 : (key < o.key) ? -1 : 1;
				}
				
				public Entry(double key, int reference) {
					this.key = key;
					this.row = reference;
				}
			}
	
			Entry[] references = new Entry[matrix.length];
			
			for (int i = 0; i < matrix.length; i++) references[i] = new Entry(computeSumOfRow(i), i);
			
			Arrays.sort(references);
			
			//move rows according to references
			
			double[][] newMatrix = new double[matrix.length][];
			
			for (int i = 0; i  < matrix.length; i++) {
				newMatrix[i] = matrix[references[i].row];
			}
			
			matrix = newMatrix;
		}
		return this;

	}
	
	public MatrixOfDouble roundValues() {
		if (matrix != null) {
			for (int i = 0; i < matrix.length; i++) {
				for (int j = 0; j < matrix[i].length; j++) matrix[i][j] = round(matrix[i][j]);
			}
		}
		return this;
	}
	
	public MatrixOfDouble rotateCounterclockwise() {
		if (matrix != null) {
			double[][] rotatedMatrix = new double[matrix[0].length][matrix.length];
			
			for (int i = 0; i < matrix.length; i++) {
				for (int j = matrix[0].length - 1; j >= 0; j--) {
					rotatedMatrix[matrix[0].length - j - 1][i] = matrix[i][j];				
				}
			}
			matrix = rotatedMatrix;
		}
		return this;
	}
	
	public MatrixOfDouble findAscendingRange() {
		if (matrix != null) {
			int range = 0, lengthRange = 0;
			Double prevValue = null;
			
			for (int i = 0; i < matrix.length; i++)
				for (int j = 0; j < matrix[i].length; j++) {
					if (prevValue != null)
						if (matrix[i][j] > prevValue) {
							lengthRange++;
							prevValue = matrix[i][j];
						} else {							// strong ascending sequence is over
							if (lengthRange > maxLength) { 
								maxRangeBegin = range;		// new max range found 
								maxLength = lengthRange;
							}
							prevValue = matrix[i][j]; 		// start new range
							range = i * matrix.length + j;
							lengthRange = 1;
						}
					else { //First element
						prevValue = matrix[0][0];
						range = 0;
						lengthRange = 1;
					}
				}
		}
		return this;
	}
	
	public int getRangeBegin() {
		return maxRangeBegin;
	}
	
	public int getRangeLength() {
		return maxLength;
	}
	
	public MatrixOfDouble cyclicRowsDownShift(int k) {
		if (matrix != null && k > 0) {
			double[][] rolledMatrix = new double[matrix.length][];
			
			for (int i = 0; i < matrix.length; i++) {
				rolledMatrix[(i + k) % matrix.length] = matrix[i];
			}
			matrix = rolledMatrix;
		}
		return this;
	}
	
	public MatrixOfDouble swapRows(int row1, int row2) {
		if (matrix != null && row1 >= 0 && row1 < matrix.length && row2 >= 0 && row2 < matrix.length) {
			double[][] newMatrix = Arrays.copyOf(matrix, matrix.length);
			
			double[] tmp = newMatrix[row1];
			newMatrix[row1] = newMatrix[row2];
			newMatrix[row2] = tmp;
			
			matrix = newMatrix;
		}
		return this;
	}

	public MatrixOfDouble swapColumns(int column1, int column2) {
		if (matrix != null && column1 >= 0 && column2 >= 0) {
			double[][] newMatrix = new double[matrix.length][];
		
			for (int i  = 0; i < matrix.length; i++) {
				double[] newRow = Arrays.copyOf(matrix[i], matrix[i].length);
				if (column1 >= newRow.length || column2 >= newRow.length) throw new IllegalStateException("Matrix must be orthogonal or column parameters are too big");
				double tmp = newRow[column1];
				newRow[column1] = newRow[column2];
				newRow[column2] = tmp;
				newMatrix[i] = newRow;
			}
			matrix = newMatrix;
		}
		return this;
	}
	
	public MatrixOfDouble deleteColumn(int column) {
		if (matrix != null && column >= 0) {
			double[][] newMatrix = new double[matrix.length][];
			
			for (int i  = 0; i < matrix.length; i++) {
				double[] newRow = new double[matrix[i].length - 1];
				if (column >= matrix[i].length) throw new IllegalStateException("Matrix must be orthogonal or column parameter is too big");
				for (int j = 0, k = 0; j < matrix[i].length; j++)
					if (j != column) newRow[k++] = matrix[i][j];
				newMatrix[i] = newRow;
			}
			matrix = newMatrix;
		}

		return this;
	}

	public MatrixOfDouble deleteRow(int row) {
		if (matrix != null && row >= 0 && row < matrix.length) {
			double[][] newMatrix = new double[matrix.length - 1][];
		
			for (int i = 0, j = 0; i < matrix.length; i++)
				if (i != row) newMatrix[j++] = matrix[i];
			matrix = newMatrix;
		}
		return this;
	}

	public boolean isZeroColumn(int column) {
		if (matrix != null && column >= 0) {
			for (int i = 0; i < matrix.length; i++) {
				if (column >= matrix[i].length) throw new IllegalStateException("Matrix must be orthogonal");
				if (matrix[i][column] != 0.0) return false;			
			}
			return true;
		}
		return false;
	}

	public boolean isZeroRow(int row) {
		if (matrix != null && row >= 0 && row < matrix.length) {
			for (double val : matrix[row])
				if (val != 0.0) return false;
			return true;
		}
		return false;
	}
	
	public void print(PrintWriter out) {
		if (out == null || matrix == null) return;
		
		for (int i = 0; i < matrix.length; i++) {
			for (int j = 0; j < matrix[i].length; j++) out.printf("%8e ", matrix[i][j]);
			out.println();
		}
	}
	
	public void printRange(PrintWriter out, int begin, int length) {
		if (out == null
			|| matrix == null
			|| begin < 0
			|| length < 0
			|| begin + length >= matrix.length * matrix.length) return; 
		
		out.printf("Range from [%d][%d] (%d elements): ", begin / matrix.length, begin % matrix.length, length);
		for (int i = begin; i < begin + length; i++) out.printf("%8e ", matrix[i / matrix.length][i % matrix.length]);
		out.println();
	}
	
	public double calculateSumBetweenPositiveNumbersInRow(int row) {
		double tailSum = 0.0;
		
		if (matrix != null && row >= 0 && row < matrix.length) {
			boolean firstFound = false;
			for (int i = 0; i < matrix[row].length; i++) {
				if (matrix[row][i] > 0)
					if (!firstFound) firstFound = true;
					else return tailSum;
				else if (firstFound) tailSum += matrix[row][i];
			}
		}
		return 0.0;
	}
	
	public double computeSumOfRow(int row) {
		double sum = 0.0;
		
		if (matrix != null && row >= 0 && row < matrix.length)
			for (double val : matrix[row]) sum += val;

		return sum;
	}
	
	public double computeCharacteristic(int column) {
		double sum = 0.0;
		
		if (matrix != null && column >=0 ){
			for (int i = 0; i < matrix.length; i++) {
				if (column < matrix[i].length) sum += abs(matrix[i][column]);
			}
		}
		return sum;
	}
	
	public MatrixOfDouble sortRowsByColumnValues(int k) {
		if (matrix != null) {
			
			class Entry implements Comparable<Entry> {
				final Double value;
				final double[] row;
				
				public Entry(double value, double[] row) {
					this.value = value;
					this.row = row;
				}
				
				@Override
				public int compareTo(Entry o) {
					return value.compareTo(((Entry)o).value);
				};
			}
			
			Entry[] reference = new Entry[matrix.length];
			
			for (int i = 0; i < matrix.length; i++) {
				if (k >= 0 && k < matrix[i].length) reference[i] = new Entry(matrix[i][k], matrix[i]);
				else throw new IllegalArgumentException("Column number must be positive and less than length of row");
			}
			
			Arrays.sort(reference);
			
			for (int i = 0; i < matrix.length; i++) matrix[i] = reference[i].row;
		}
		return this;
	}

	public int getNumberOfRows() {
		if (matrix != null) return matrix.length;
		return 0;
	}

	public double[] getRow(int row) {
		if (matrix != null && row >= 0 && row < matrix.length) return matrix[row];
		return null;
	}

	public MatrixOfDouble floatZeroValuesInRows() {
		if (matrix != null) {
			for (int i = 0; i < matrix.length; i++) {
				double[] newRow = new double[matrix[i].length];
				int k = 0;
				for (int j = 0; j < matrix[i].length; j++)
					if (matrix[i][j] != 0.0) newRow[k++] = matrix[i][j];
				matrix[i] = newRow;
			}
		}
		return this;
	}

	public int countLocalMinimums() {
		int count = 0;
		if (matrix != null) {
			for (int i = 0; i < matrix.length; i++)
				for (int j = 0; j < matrix[i].length; j++)
					if (isLocalMimimum(i, j)) count++;
			
		}
		return count;
	}

	public boolean isLocalMimimum(int i, int j) {
		if (matrix == null || i < 0 || j < 0 || i >= matrix.length || j >= matrix[i].length) return false;
		
		VectorValuesFinder neighborhood = new VectorValuesFinder(getNeighborhood(i, j));
		return neighborhood.getMinValue() > matrix[i][j];
	}

	protected double[] getNeighborhood(int i, int j) {
		int[][] shift = {{-1, -1}, {-1, 0}, {-1, 1}, {0, 1}, {1, 1}, {1, 0}, {1, -1}, {0, -1}};
		
		double[] neighborhood = new double[8];
		int count = 0;
		for (int k = 0; k < shift.length; k++) {
			try {
				neighborhood[count++] = matrix[i + shift[k][0]][j + shift[k][1]];
			} catch (ArrayIndexOutOfBoundsException e) {
				continue;
			}
		}
		neighborhood = Arrays.copyOf(neighborhood, count);
		return neighborhood;
	}
	
	public boolean isLocalMaximum(int i, int j) {
		if (matrix == null || i < 0 || j < 0 || i >= matrix.length || j >= matrix[i].length) return false;
		
		VectorValuesFinder neighborhood = new VectorValuesFinder(getNeighborhood(i, j));
		return neighborhood.getMaxValue() < matrix[i][j];
	}
	
	public Double getTheBiggestLocalMaximum() {
		Double value = null;
		if (matrix != null) {
			for (int i = 0; i < matrix.length; i++)
				for (int j = 0; j < matrix[i].length; j++)
					if (isLocalMaximum(i, j))
						if (value == null) value = matrix[i][j];
						else if (value < matrix[i][j]) value = matrix[i][j];
		}
		return value;
	}

	public int countSaddlePoints() {
		int count = 0;
		
		if (matrix != null) {
			double[] rowsMinimums = getMinimumsOfRows();
			double[] columnsMaximum = getMaximumsOfColumns();
			for (int i = 0; i < matrix.length; i++)
				for (int j = 0; j < matrix[i].length; j++)
					if (matrix[i][j] == rowsMinimums[i] && matrix[i][j] == columnsMaximum[j]) count++;
		}
		return count;
	}

	protected double[] getMaximumsOfColumns() {
		double[] maximums = null;
		if (matrix != null && matrix[0] != null) {
			maximums = new double[matrix[0].length];
			for (int i = 0; i < maximums.length; i++) {
				double[] column = getColumn(i);
				VectorValuesFinder finder = new VectorValuesFinder(column);
				maximums[i] = finder.getMaxValue();
			}
		}

		return maximums;
	}

	public double[] getColumn(int column) {
		double[] vector = null;
		if (matrix != null && column >= 0) {
			vector = new double[matrix.length];
			for (int i = 0; i < matrix.length; i++)
				if (matrix[i] != null && column < matrix[i].length) vector[i] = matrix[i][column];
				else throw new IllegalArgumentException("Matrix must be orthogonal and column argument must be less than length of rows");
		}
		return vector;
	}

	public double[] getMinimumsOfRows() {
		double[] minimums = null;
		if (matrix != null) {
			VectorValuesFinder[] minOfRows = new VectorValuesFinder[matrix.length];
			
			for (int i = 0; i < matrix.length; i++) minOfRows[i] = new VectorValuesFinder(matrix[i]);
			
			minimums = new double[minOfRows.length];
			for (int i = 0; i < minOfRows.length; i++) minimums[i] = minOfRows[i].getMinValue(); 
		}

		return minimums;
	}

}